package com.wzh.glsc.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wzh.common.utils.PageUtils;
import com.wzh.common.utils.Query;
import com.wzh.glsc.product.dao.CategoryDao;
import com.wzh.glsc.product.entity.CategoryEntity;
import com.wzh.glsc.product.service.CategoryBrandRelationService;
import com.wzh.glsc.product.service.CategoryService;
import com.wzh.glsc.product.vo.CategoryLeve2Vo;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Resource
    CategoryBrandRelationService categoryBrandRelationService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        List<CategoryEntity> list = baseMapper.selectList(null);
        List<CategoryEntity> tree = list.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                .map(categoryEntity -> {
                    categoryEntity.setChildren(getChildrens(categoryEntity, list));
                    return categoryEntity;
                }).sorted(Comparator.comparingInt(menu -> menu.getSort() == null ? 0 : menu.getSort()))
                .collect(Collectors.toList());
        return tree;
    }

    @Override
    public void removeByMenuIds(List<Long> catIds) {
        //TODO 检查当前删除的菜单 是否被别的地方引用
        baseMapper.deleteBatchIds(catIds);
    }

    @Override
    public Long[] getCatelogPathBycatId(Long catelogId) {
        List<Long> catelogPathList = new ArrayList<>();
        getCatelogPath(catelogPathList, catelogId);
        Collections.reverse(catelogPathList);
        return catelogPathList.toArray(new Long[catelogPathList.size()]);
    }

    //失效模式,多次操作
    @CacheEvict(value = "category",allEntries = true)
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        //TODO 分类修改操作
        baseMapper.updateById(category);
        //修改分类与商品关联表中的分类信息
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }


    @Cacheable(value = "category", key = "#root.methodName",sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", 1));
    }


    //用SpringCache 缓存数据到redis
    //这里括号内写放入缓存的分区
    //key是自动生成的： SimpleKey[]
    //默认使用jdk序列化，将序列化后的数据存到redis
    //默认使用的过期时间TTL是-1,不符合业务设定
    //
    //自定义：指定缓存生成的key：
    //                  接受SqEl表达式：
    //                     key="'category_levels_tree_rwlock'"
    //                     可以去官网看表达式有哪些
    //      生成的存活时间：在配置文件中修改TTL 毫秒 spring.cache.redis.time-to-live=3600000
    //      将数据存为json：
    @Cacheable(value = {"category"}, key = "#root.method.name")
    @Override
    public Map<String, List<CategoryLeve2Vo>> getCategoryJson() {
        System.out.println("缓存未击中");
        return getCatagoryJsonFromDb();
    }

    //分布式锁，从数据库中查出并封装好主页的三级分类
    public Map<String, List<CategoryLeve2Vo>> getCategoryJsonFormDbWithRedisson() {
        RReadWriteLock category_levels_tree_lock = redissonClient.getReadWriteLock("category_levels_tree_rwlock");
        RLock rLock = category_levels_tree_lock.writeLock();
        rLock.lock(36000000 + new Double(Math.random() * 300000).longValue(), TimeUnit.SECONDS);
        try {
            System.out.println("抢到锁了,开始业务..");
            //TODO 这里还没配置设置应对缓存穿透的问题
            return getCatagoryJsonFromDb();
        } finally {
            rLock.unlock();
        }
    }


    //分布式锁，从数据库中查出并封装好主页的三级分类
    public Map<String, List<CategoryLeve2Vo>> getCategoryJsonFormDbWithRedis() {
        //用uuid设置一个专属自己的token
        String uuid = UUID.randomUUID().toString();
        //抢占锁，并设置过期时间; 并把锁的value设置为专属自己的token，
        // 保证锁的原子性
        // 设置过期时间是为了避免 在解锁时，因为其他原因没有解锁成功时，时间到了redis会自动解锁
        //设置token是为了防止在解锁时，自己的锁释被redis放掉了，解了别的进程的锁
        Boolean lockState = stringRedisTemplate.opsForValue().setIfAbsent("categoryJsonLock",
                uuid, 300, TimeUnit.SECONDS);
        //抢占锁成功就执行业务
        if (lockState) {
            try {
                //执行业务
                //TODO  业务没完成时,如何自动续锁时间?
                System.out.println("抢到锁了,开始业务..");
                return getCatagoryJsonFromDb();
            } finally {
                //不管业务是否有异常都必须用finally解锁
                // 用Lua脚本解自己的锁；保证解锁的原子性
                //如果再去redis获取自己设置的锁来解。可能获取到了自己的
                //      但在发出解锁的请求时，可能因为自己的锁被 redis释放掉了
                //      从而误解了别的线程的锁，所以要用Lua脚本
                String script = "if redis.call('get',KEYS[])==ARGV[1] " +
                        "               then return redis.call('del',KEY[1])" +
                        "   else" +
                        "       return 0" +
                        " end";
                //categoryJsonLockState是删除是否成功,成功是1 失败为0
                //execute()方法中参数:
                //          new 一个DefaultRedisScript<>(Lua脚本:script,返回的参数类),
                //          要解锁的锁key,这里需要放一个集合,
                //          专属自忆的token值,也就是上面声明的uuid
                Long categoryJsonLockState = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                        Arrays.asList("categoryJsonLock"), uuid);
                System.out.println("解锁,解锁返回状态" + categoryJsonLockState);
            }
        } else {
            try {
                //没抢到锁则睡眠一会再抢占锁
                //Thread.sleep 必须抛出异常
                //这里睡眠了200毫秒
                System.out.println("没抢到锁,休息中...");
                Thread.sleep(200);
            } catch (Exception e) {
            }
            //睡眠一会后,再调用自己,
            //TODO  这里 我觉得就该调用上面的业务方法
            // 不该调用自己,但调用别的方法会嵌套很多层,不知道会不会出问题??
            return getCategoryJsonFormDbWithRedis();
        }

    }

    private Map<String, List<CategoryLeve2Vo>> getCatagoryJsonFromDb() {
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
        //一级CategoryEntity
        List<CategoryEntity> categoryLevel1List = getParent_id(categoryEntities, 0L);
        Map<String, List<CategoryLeve2Vo>> collect = categoryLevel1List.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
            //二级CategoryEntity
            List<CategoryEntity> categoryLevel2List = getParent_id(categoryEntities, l1.getCatId());
            List<CategoryLeve2Vo> categoryLeve2Vos = null;
            if (categoryLevel1List != null) {
                //二级CategoryLeve2Vo
                categoryLeve2Vos = categoryLevel2List.stream().map(l2 -> {
                    //三级CategoryEntity
                    List<CategoryEntity> categoryLevel3List = getParent_id(categoryEntities, l2.getCatId());
                    List<CategoryLeve2Vo.CategoryLeve3Vo> categoryLeve3Vos = null;
                    //三级CategoryLeve3Vo
                    if (categoryLevel1List != null) {
                        categoryLeve3Vos = categoryLevel3List.stream().map(l3 -> {
                            return new CategoryLeve2Vo.CategoryLeve3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        }).collect(Collectors.toList());
                    }
                    return new CategoryLeve2Vo(l1.getCatId().toString(), categoryLeve3Vos, l2.getCatId().toString(), l2.getName());
                }).collect(Collectors.toList());
            }
            return categoryLeve2Vos;
        }, (c1, c2) -> c2));

//        //缓存穿透解决
//        if (collect == null || collect.size() == 0) {
//            stringRedisTemplate.opsForValue().set("catalogJSON",
//                    "0", Math.abs(new Random().nextInt(10)), TimeUnit.DAYS);
//        } else {
//            //转成json放入缓存中
//            //缓存雪崩解决
//            stringRedisTemplate.opsForValue().set("catalogJSON",
//                    JSON.toJSONString(collect), Math.abs(new Random().nextInt(10)),
//                    TimeUnit.DAYS);
//        }
        return collect;
    }

    //本地锁，从数据库中查出并封装好主页的三级分类
    public Map<String, List<CategoryLeve2Vo>> getCategoryTree() {
        //加上本地锁,this指自己,自已是单例,
        //也可以直接写在public 后面 :public synchronized Map<String, List<CategoryLeve2Vo>> getCategoryTree()
        //要保证原子性, 从查库到存入缓存后再释放锁
        synchronized (this) {
            //从缓存中取出存入的 catalogJSON,看是否有其他线程是否已经存入缓存中
            String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
            //判断缓存中是否有值
            if (StringUtils.isNotEmpty(catalogJSON)) {
                System.out.println("缓存击中");
                //有就直接入redis出根据key取出来,不再去数据库查数据
                return JSON.parseObject(catalogJSON,
                        new TypeReference<Map<String, List<CategoryLeve2Vo>>>() {
                        });
            }

            System.out.println("缓存未击中");
            return getCatagoryJsonFromDb();
        }
    }

    //筛选参数 list中 ，身符合 parentId 的数据
    private List<CategoryEntity> getParent_id(List<CategoryEntity> list, Long parentId) {
//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", i).eq("parent_cid", l1.getCatId()));
        return list.stream().filter(category -> category.getParentCid().equals(parentId)).collect(Collectors.toList());
    }

    private List<Long> getCatelogPath(List<Long> catelogPathList, Long catelogId) {
        catelogPathList.add(catelogId);
        CategoryEntity categoryEntity = baseMapper.selectById(catelogId);
        if (categoryEntity.getParentCid() != 0) {
            getCatelogPath(catelogPathList, categoryEntity.getParentCid());
        }
        return catelogPathList;
    }


    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> categoryEntityList) {
        return categoryEntityList.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == root.getCatId())
                .map(categoryEntity -> {
                    categoryEntity.setChildren(getChildrens(categoryEntity, categoryEntityList));
                    return categoryEntity;
                }).sorted(Comparator.comparingInt(menu -> menu.getSort() == null ? 0 : menu.getSort()))
                .collect(Collectors.toList());
    }

}